package com.jackrain.nea.ext.oss;


import com.alibaba.fastjson.JSON;
import com.aliyun.oss.OSSClient;
import com.aliyun.oss.event.ProgressEvent;
import com.aliyun.oss.event.ProgressEventType;
import com.aliyun.oss.event.ProgressListener;
import com.aliyun.oss.model.*;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.time.FastDateFormat;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.text.MessageFormat;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;

import static java.util.concurrent.Executors.newFixedThreadPool;

/**
 * Oss操作帮助类
 *
 * @author blqw
 * @date 2017-12
 */
@Slf4j
public class OssHelper {

    /**
     * 路径分隔符
     */
    static final String PATH_SEPARATION = "/";
    /**
     * 待修复的错误的路径分隔符
     */
    static final String REPAIR_SEPARATION = "[\\\\/]+";
    /**
     * http://
     */
    static final String PROTOCOL_HTTP = "http://";
    /**
     * https://
     */
    static final String PROTOCOL_HTTPS = "https://";

    static final String DOT_SYMBOL = ".";

    static final String EMPTY_STRING = "";

    /**
     * Oss分段上传最小大小
     */
    static final int OSS_PART_MINSIZE = 100;

    /**
     * OSS域名映射
     */
    final static HashMap<String, String> domainMappings = new HashMap<>();

    public final OSSClient ossClient;
    public final String endpoint;
    private final String domain;
    @Getter
    @Setter
    public FastDateFormat dateFormatter = FastDateFormat.getInstance("yyMMddHHmmss");
    /**
     * 路径自动转小写
     */
    @Getter
    @Setter
    public boolean autoToLower = false;
    /**
     *
     */
    @Getter
    @Setter
    public int loadObjectBatchCount = 100;
    @Getter
    private final String bucket;

    private static void validString(String name, String value)
            throws NullPointerException {
        if (value == null || value.length() == 0) {
            throw new NullPointerException(MessageFormat.format("{0} is {1}", name, value == null ? "null" : "empty"));
        }
    }

    private static Object ifNull(Object value, Object defaultValue) {
        return value == null ? defaultValue : value;
    }

    /**
     * 添加域名映射
     *
     * @param src    源域名
     * @param target 目标域名
     */
    public static void putDomainMapping(String src, String target) {
        domainMappings.put(src, target);
    }

    /**
     * 移除域名映射
     *
     * @param src 源域名
     */
    public static void removeDomainMapping(String src) {
        domainMappings.remove(src);
    }

    /**
     * 清空域名映射
     */
    public static void clearDomainMapping() {
        domainMappings.clear();
    }

    /**
     * 域名映射
     *
     * @param domain
     * @return
     */
    public String domainMap(String domain) {
        validString("domain", domain);
        domain = domain.trim();
        if (autoToLower) {
            domain = domain.toLowerCase();
        }
        String str = domainMappings.getOrDefault(domain, null);
        if (str != null) {
            return str;
        }

        if (domain.startsWith(PROTOCOL_HTTP)) {
            str = domainMappings.getOrDefault(domain.substring(PROTOCOL_HTTP.length()), null);
        } else if (domain.startsWith(PROTOCOL_HTTPS)) {
            str = domainMappings.getOrDefault(domain.substring(PROTOCOL_HTTPS.length()), null);
        } else {
            return PROTOCOL_HTTP + domain;
        }
        return str == null ? domain : str;
    }

    /**
     * @param key
     * @return
     */
    public String keyFix(String key) {
        //将路径 前后去空格 转小写 将路径中的 \ 替换为 /
        key = ifNull(key, EMPTY_STRING).toString().trim().replaceAll(REPAIR_SEPARATION, PATH_SEPARATION);
        if (autoToLower) {
            key = key.toLowerCase();
        }
        if (key.charAt(0) == PATH_SEPARATION.charAt(0)) {
            key = key.substring(1);
        }
        return key;
    }

    /**
     * @param path
     * @return
     */
    public String pathFix(String path) {
        return keyFix(path + PATH_SEPARATION);
    }

    private static String getExtension(String nameOrPath) {
        int i = nameOrPath.lastIndexOf(DOT_SYMBOL);
        return i < 0 ? EMPTY_STRING : nameOrPath.substring(i);
    }

    /**
     * 获取一个随机不重复的文件名
     *
     * @param ext 文件扩展名, 可以为null或空字符串
     */
    private String getRandomFileName(String ext) {
        if (ext != null && ext.length() > 0) {
            ext = ext.trim();
            if (autoToLower) {
                ext = ext.toLowerCase();
            }
            if (ext.length() > 0 && !ext.startsWith(DOT_SYMBOL)) {
                ext = DOT_SYMBOL + ext;
            }
        } else {
            ext = EMPTY_STRING;
        }

        String fileName;
        do {
            //如果文件名重复则重新随机一个名称
            String guid = UUID.randomUUID().toString();
            fileName = guid + ext;
        } while (exists(fileName));
        return fileName;
    }

    /**
     * 创建上传文件的元数据
     *
     * @param fileName
     * @param tag
     * @param props
     * @return
     */
    private ObjectMetadata createMetadata(String fileName, String tag, Object props) {
        // 创建上传Object的Metadata
        ObjectMetadata meta = new ObjectMetadata();
        // 设置下载文件名
        meta.setContentDisposition("attachment;filename=" + fileName);
        meta.addUserMetadata("name", fileName);
        meta.addUserMetadata("id", fileName);
        meta.addUserMetadata("ext", getExtension(fileName));
        if (tag != null && tag.length() > 0) {
            meta.addUserMetadata("tag", tag);
        }
        if (props != null) {
            meta.addUserMetadata("props", JSON.toJSONString(props));
        }
        return meta;
    }

    /**
     * 将元数据转为Map
     *
     * @param metadata
     * @return
     */
    private Map<String, String> convertMap(ObjectMetadata metadata) {
        if (metadata == null) {
            return null;
        }
        HashMap<String, String> map = new HashMap<>();
        Map<String, Object> raw = metadata.getRawMetadata();
        for (String s : raw.keySet()) {
            Object val = raw.get(s);
            if (val != null) {
                map.put(s, String.valueOf(val));
            }
        }
        map.putAll(metadata.getUserMetadata());
        return map;
    }

    /**
     * 初始化Oss操作帮助类
     *
     * @param accessKeyId     阿里云操作的 accessKey
     * @param accessKeySecret 阿里云操作的 secretKey
     * @param endpoint        阿里云OSS域名
     * @throws OssConnectException  阿里云连接失败
     * @throws NullPointerException 参数错误
     */
    public OssHelper(String accessKeyId, String accessKeySecret, String endpoint, String bucket)
            throws RuntimeException, NullPointerException {
        validString("accessKeyId", accessKeyId);
        validString("accessKeySecret", accessKeySecret);
        validString("endpoint", endpoint);
        validString("bucket", bucket);

        this.endpoint = endpoint.trim().toLowerCase();
        this.bucket = bucket.trim().toLowerCase();
        if (endpoint.startsWith(PROTOCOL_HTTP)) {
            this.domain = domainMap(PROTOCOL_HTTP + bucket + DOT_SYMBOL + endpoint.substring(PROTOCOL_HTTP.length()));
        } else if (endpoint.startsWith(PROTOCOL_HTTPS)) {
            this.domain = domainMap(PROTOCOL_HTTPS + bucket + DOT_SYMBOL + endpoint.substring(PROTOCOL_HTTPS.length()));
        } else {
            this.domain = domainMap(bucket + DOT_SYMBOL + endpoint);
        }

        try {
            ossClient = new OSSClient(endpoint, accessKeyId, accessKeySecret);
        } catch (Exception ex) {
            throw new OssConnectException(accessKeyId, accessKeySecret, endpoint, bucket, ex);
        }
    }


    /**
     * 上传文件
     *
     * @param path     上传路径
     * @param fileName 上传文件名称
     * @param content  文件内容
     * @return
     */
    public OssResult upload(String path, String fileName, byte[] content) {
        return upload(path, fileName, new ByteArrayInputStream(content));
    }

    /**
     * 上传文件
     *
     * @param path     上传路径
     * @param fileName 上传文件名称
     * @param stream   文件流
     * @return
     */
    public OssResult upload(String path, String fileName, InputStream stream)
            throws NullPointerException {
        return upload(path, fileName, stream, null);
    }

    public OssResult upload(String path, String fileName, InputStream stream, String ossKey)
            throws NullPointerException {
        if (stream == null) {
            throw new NullPointerException("stream is null");
        }
        validString("fileName", fileName);
        fileName = fileName.trim();
        path = pathFix(path);
        String key = null;


        try {
            //这里获取随机文件名可以保证不重复
            key = path + (ossKey == null ? getRandomFileName(getExtension(fileName)) : ossKey);

            // 创建上传Object的Metadata
            ObjectMetadata meta = createMetadata(fileName, null, null);

            PutObjectResult result = ossClient.putObject(bucket, key, stream, meta);
            log.debug("upload eTag = " + result.getETag());
            return new OssResult(endpoint, domain, bucket, key, result.getETag(), null, null);
        } catch (Exception ex) {
            return new OssResult(endpoint, domain, bucket, key, null, ex, null);
        }
    }

    /**
     * 分段上传文件
     *
     * @param path           上传路径
     * @param fileName       上传文件名称
     * @param stream         文件流
     * @param partSizeKB     分段大小(单位kb)
     * @param parallelsCount 并行线程数
     * @return
     */
    public OssResult SectionUpload(String path, String fileName, InputStream stream, int partSizeKB, int parallelsCount)
            throws IllegalArgumentException {
        if (partSizeKB < OSS_PART_MINSIZE) {
            throw new IllegalArgumentException("pratSize too small (<" + OSS_PART_MINSIZE + ")");
        }
        if (parallelsCount < 1) {
            throw new IllegalArgumentException("parallelsCount too small (<1)");
        }
        validString("fileName", fileName);
        fileName = fileName.trim();
        //将路径 前后去空格 转小写 将路径中的 \ 替换为 /
        path = pathFix(path);
        String key = path + getRandomFileName(getExtension(fileName));

        // 创建上传Object的Metadata
        ObjectMetadata meta = createMetadata(fileName, null, null);
        InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(bucket, key, meta);

        InitiateMultipartUploadResult result = ossClient.initiateMultipartUpload(request);
        String uploadId = result.getUploadId();
        ExecutorService executorService = newFixedThreadPool(parallelsCount);
        List<PartETag> partETags = Collections.synchronizedList(new ArrayList<PartETag>());

        int offset = 0;
        int partCount = 0;

        UploadProgress progress = new UploadProgress(key);
        while (true) {
            //循环开启线程任务
            byte[] part = new byte[partSizeKB * 1024];
            int readBytes;
            try {
                readBytes = stream.read(part, offset, part.length);
            } catch (IOException e) {
                return new OssResult(endpoint, domain, bucket, key, null, e, partETags);
            }
            //增加总上传数
            progress.addTotalBytes(readBytes);
            partCount++;
            int partNumber = partCount;

            if (partCount > 10000) {
                progress.close();
                executorService.shutdownNow();
                return new OssResult(endpoint, domain, bucket, key, null, new RuntimeException("Total parts count should not exceed 10000"), partETags);
            }

            executorService.execute(() -> {
                int maxTryCount = 3;
                for (int j = 1; j <= maxTryCount; j++) {
                    try {
                        UploadPartRequest uploadPartRequest = new UploadPartRequest();
                        uploadPartRequest.setBucketName(bucket);
                        uploadPartRequest.setKey(key);
                        uploadPartRequest.setUploadId(uploadId);

                        uploadPartRequest.setInputStream(new ByteArrayInputStream(part));
                        // 设置分片大小，除最后一个分片外，其它分片要大于100KB
                        uploadPartRequest.setPartSize(readBytes);
                        // 设置分片号，范围是1~10000，
                        uploadPartRequest.setPartNumber(partNumber);
                        // 设置上传进度监听器
                        uploadPartRequest.<PutObjectRequest>withProgressListener(progress);
                        UploadPartResult uploadPartResult = ossClient.uploadPart(uploadPartRequest);
                        log.debug("Part#" + partNumber + " done");
                        synchronized (partETags) {
                            partETags.add(uploadPartResult.getPartETag());
                        }
                        //全部上传完成
                        if (progress.isFixed() && partETags.size() == progress.getPartCount()) {
                            Collections.sort(partETags, Comparator.comparingInt(PartETag::getPartNumber));
                            CompleteMultipartUploadRequest completeMultipartUploadRequest =
                                    new CompleteMultipartUploadRequest(bucket, key, uploadId, partETags);
                            ossClient.completeMultipartUpload(completeMultipartUploadRequest);
                            progress.combined();
                            progress.close();
                        }
                        return;
                    } catch (Exception e) {
                        log.error(MessageFormat.format("upload failed at uploadId:{0}, part:{1}, tried :{2}", uploadId, partNumber, j), e);
                        if (j == maxTryCount) {
                            progress.close();
                            executorService.shutdownNow();
                        }
                    }
                }
            });
            if (readBytes < part.length) {
                log.debug("Total parts count " + partCount);
                break;
            }
        }
        progress.setFixed(true);

        executorService.shutdown();
        return new OssResult(endpoint, domain, bucket, key, null, null, null);
    }

    /**
     * 返回上传进度 0~100  (-1表示上传失败)
     *
     * @param key 指定查询的资源key
     * @return
     */
    public double uploadProgress(String key) {
        validString("key", key);
        key = keyFix(key);
        if (ossClient.doesObjectExist(bucket, key)) {
            return 100;
        }
        UploadProgress progress = UploadProgress.get(key);
        if (progress == null) {
            return -1;
        }
        return Math.min(progress.getPercent(), 99.99999999999999);

    }

    /**
     * 上传进度
     */
    static class UploadProgress implements ProgressListener {

        static ConcurrentHashMap<String, UploadProgress> map = new ConcurrentHashMap<>();

        public static UploadProgress get(String key) {
            return map.getOrDefault(key, null);
        }

        private long uploadedBytes = 0;
        @Getter
        @Setter
        private long totalBytes = -1;
        @Getter
        private boolean succeed = false;
        @Getter
        private boolean failed = false;
        @Getter
        @Setter
        private boolean fixed = false;

        @Getter
        private int partCount;

        private String key;

        public UploadProgress(String key) {
            this.key = key;
            map.put(key, this);
        }

        private double getPercent() {
            //上传字节增加1个虚拟字节 在调用combined方法时增加, 表示分片合并完成
            return (this.uploadedBytes + 1) * 100.0 / this.totalBytes;
        }

        public void addTotalBytes(int bytes) {
            partCount++;
            totalBytes = totalBytes == -1 ? bytes : totalBytes + bytes;
        }

        public void combined() {
            this.totalBytes++;
        }

        @Override
        public void progressChanged(ProgressEvent progressEvent) {
            long bytes = progressEvent.getBytes();
            ProgressEventType eventType = progressEvent.getEventType();
            switch (eventType) {
                case TRANSFER_STARTED_EVENT:
                    break;
                case REQUEST_BYTE_TRANSFER_EVENT:
                    this.uploadedBytes += bytes;
                    break;
                case TRANSFER_COMPLETED_EVENT:
                    this.succeed = true;
                    break;
                case TRANSFER_FAILED_EVENT:
                    this.failed = true;
                    break;
                default:
                    break;
            }
        }

        public void close() {
            map.remove(key);
        }
    }

    /**
     * 删除文件或文件夹
     *
     * @param key 文件或文件夹标识(文件夹必须以"/"结尾)
     */
    public void delete(String key) {
        validString("key", key);
        key = keyFix(key);
        if (key.endsWith(PATH_SEPARATION)) {
            deleteFolder(key);
            return;
        }
        ossClient.deleteObject(bucket, key);
    }

    /**
     * 删除文件夹
     *
     * @param folder 文件夹标识(会自动补全文件夹结尾的"/"符号)
     */
    public void deleteFolder(String folder) {
        validString("folder", folder);
        folder = pathFix(folder);

        final int maxKeys = 1000;
        List<String> keys = new ArrayList<>();
        for (OssObject ossObject : list(folder, true)) {
            keys.add(ossObject.getKey());
            if (keys.size() == maxKeys) {
                ossClient.deleteObjects(new DeleteObjectsRequest(bucket).withKeys(keys));
                keys.clear();
            }
        }
        if (keys.size() > 0) {
            ossClient.deleteObjects(new DeleteObjectsRequest(bucket).withKeys(keys));
            keys.clear();
        }
        ossClient.deleteObject(bucket, folder);
    }

    /**
     * 判断文件或文件夹是否存在
     *
     * @param key 文件或文件夹标识(文件夹必须以"/"结尾)
     * @return
     */
    public boolean exists(String key) {
        if (key == null || key.length() == 0) {
            return false;
        }
        key = keyFix(key);
        if (!key.endsWith(PATH_SEPARATION)) {
            return ossClient.doesObjectExist(bucket, key);
        }
        key = pathFix(key);
        ListObjectsRequest request = new ListObjectsRequest(bucket);
        request.setPrefix(key);
        request.setMaxKeys(1);
        ObjectListing objects = ossClient.listObjects(request);
        return objects.getObjectSummaries().size() > 0;
    }

    /**
     * 移动文件, (技术原因, 暂时只能移动1G以下的文件)
     *
     * @param srcKey   来源文件标识
     * @param destPath 目标文件夹
     * @return
     */
    public OssResult move(String srcKey, String destPath) {
        validString("srcKey", srcKey);
        validString("destPath", destPath);
        srcKey = keyFix(srcKey);
        destPath = pathFix(destPath);
        String destKey = destPath + getRandomFileName(getExtension(srcKey));
        try {
            CopyObjectResult result = ossClient.copyObject(bucket, srcKey, bucket, destKey);
            delete(srcKey);
            return new OssResult(endpoint, domain, bucket, destKey, result.getETag(), null, null);
        } catch (Exception e) {
            return new OssResult(endpoint, domain, bucket, destKey, null, e, null);
        }
    }


    public boolean copy(String srcKey, String destKey) {
        validString("srcKey", srcKey);
        validString("destKey", destKey);
        srcKey = keyFix(srcKey);
        destKey = destKey.trim();
        if (destKey.charAt(destKey.length() - 1) == PATH_SEPARATION.charAt(0)) {
            int i = srcKey.lastIndexOf(PATH_SEPARATION);
            String name = i < 0 ? srcKey : srcKey.substring(i + 1);
            destKey = destKey + name;
        }
        try {
            ossClient.copyObject(bucket, srcKey, bucket, destKey);
            return true;
        } catch (Exception e) {
            log.error(this.getClass().getName() + ".copy 出现错误:" + e.getMessage(), e);
        }
        return false;
    }


    /**
     * @param path
     * @param subFolder
     * @return
     */
    public OssObjectEnumerable list(String path, boolean subFolder) {
        validString("path", path);
        final String prefix = pathFix(path);
        final int batch = Math.min(loadObjectBatchCount, 1000);
        return new OssObjectEnumerable(new OssObjectLoader() {
            String nextMarker = null;
            boolean more = true;

            @Override
            public void load(List<OssObject> objects) {
                ListObjectsRequest request = new ListObjectsRequest(bucket);
                request.setPrefix(prefix);
                request.setMarker(nextMarker);
                if (!subFolder) {
                    request.setDelimiter(PATH_SEPARATION);
                }
                request.setMaxKeys(batch);
                ObjectListing list = ossClient.listObjects(request);

                for (OSSObjectSummary s : list.getObjectSummaries()) {
                    objects.add(new OssObject(bucket, domain, s.getKey(), x -> convertMap(ossClient.getObjectMetadata(bucket, x.getKey()))));
                }
                nextMarker = list.getNextMarker();
                more = list.isTruncated();
            }

            @Override
            public boolean isMore() {
                return more;
            }
        });
    }

    /**
     * 获取文件信息
     *
     * @param key 文件标识
     */
    public OssObject getInfo(String key) {
        validString("key", key);
        key = keyFix(key);
        ObjectMetadata metadata = ossClient.getObjectMetadata(bucket, key);
        return new OssObject(bucket, domain, key, convertMap(metadata));
    }

    /**
     * 保存oss文件到指定的文本路径
     *
     * @param key
     * @param local
     */
    public void save(String key, String local) {
        validString("key", key);
        key = keyFix(key);
        ossClient.getObject(new GetObjectRequest(bucket, key), new File(local));
    }
}
